Skip to content

feat(profiles): security-pentest, recon and scanning inside an engagement (step 5, PR 5) - #21

Merged
mkhellat merged 6 commits into
mainfrom
feat/security-pentest
Nov 12, 2026
Merged

mkhellat merged 6 commits into
mainfrom
feat/security-pentest

Conversation

@mkhellat

Copy link
Copy Markdown
Owner

Phase 10, step 5 plan, PR 5: ships the security-pentest profile, covering tiers T0 (lookups) and T1 (scanning). Exploitation (T2) waits for the scope fence in PR 6.

Commits

  1. 8ca7b07 A profile can require an open engagement. With the profile key engagement = true, command and web search steps are refused unless a registered step check provides "engagement". add_step_check(check, provides=…) records what a check provides, and the scope guard provides "engagement" while it is active. The core asks only for that name, so it still never imports codebud_sec. Without this, security-pentest would run nmap unchecked until PR 7 adds codebud-sec.
  2. 18c8ab6 nmap scripts are judged by nmap's own script.db.
    • The guard works out which scripts a --script expression selects, as nmap does: names, categories, globs, all, and/or/not, and commas.
    • The step is refused if any selected script is marked exploit, brute, dos, intrusive, fuzzer, broadcast or external, or is a targets-* script.
    • Found on the way: nmap's default category holds two external scripts (http-open-proxy, socks-open-proxy). So -sC and -A are refused, with the hint --script "default and not external".
    • -sV stays allowed, although nmap marks one version script (iec61850-mms) intrusive. This is documented.
    • Without script.db, --script, -sC and -A are refused.
    • This replaces the word matching from Engagement scope guard: every network target read and checked (DNS, scanners, nc) #18.
  3. fe9ce34 Three more rules:
    • a whois parser (passive);
    • masscan --rate capped at 1000 packets/s;
    • curl and wget may not send a file to a target (-T, @file data, form fields and headers, --post-file, --body-file).
  4. bea0efb The profile.
    • Settings: mode ask, privacy private (locked refuses network steps), encrypted, integrity required, engagement = true.
    • Allow-list: reading results is harmless; every network tool is powerful, so it is asked each time.
    • Refused by the profile: listeners (nc -l, ncat --listen, --broker, --chat).
    • Hydra, sqlmap and the other exploitation tools are not on the list.

Verification

  • make check: 1738 passed. make distcheck passes; the info and HTML manuals build without warnings; ruff is clean.
  • Each refusal in the new tests was checked to fire for its stated reason.
  • On this machine's nmap 7.99 database, -sC lists the two external scripts, and vuln, http-* and not safe are refused. smb-vuln-ms17-010 and vuln and safe and not external run.

Phase 10, step 5 plan, PR 5.  The security-pentest profile's tools
reach other hosts, and only the scope guard of an open engagement reads
which; until one is open (the codebud-sec command, PR 7) such a profile
must run no network tool at all.

A profile key, engagement = true, refuses command and web search steps
unless a registered step check provides "engagement".  add_step_check()
takes what a check provides, and the scope guard provides it while
active.  The core still knows nothing of the layer: it asks only whether
some registered check provides the name.

* src/codebud/profiles.py (_BOOL_KEYS): Add engagement.
(Profile.engagement): New field.
(Profile.summary, _load_file): Show and read it.
* src/codebud/safety.py (_PROVIDES, provided, _engagement_refusal): New.
(add_step_check): Take what the check provides.
(remove_step_check): Forget it.
(SafetyEngine.refusal_reason): Refuse without a required engagement.
* src/codebud_sec/scope.py (ScopeGuard.activate): Provide "engagement".
* tests/test_step_checks.py, tests/test_scope.py: Test them.
* docs/chapters/configuration.texi (Profiles): Document the key.
* docs/chapters/safety-and-sandbox.texi (The Safety Engine): And what a
check provides.
Phase 10, step 5 plan, PR 5 (scanning tier, T1).  Which scripts a
--script expression selects is now worked out the way nmap does it --
names, categories, globs, all, and/or/not, commas -- from nmap's
script.db, and the step is refused when any selected script is in a
category beyond scanning (exploit, brute, dos, intrusive, fuzzer) or
reaching past the command line (broadcast, external, targets-*).

This replaces matching words of the expression, which refused
"default and not broadcast"-style exclusions and could not see that
the vuln category holds exploit and dos scripts.  It also shows that
nmap's own default category holds two external scripts
(http-open-proxy, socks-open-proxy), so -sC and -A are refused with a
hint: --script "default and not external".  Script files run unchecked
code and are refused; so are names nmap does not know.  Without
script.db no script can be judged, and --script, -sC and -A are
refused.  -sV stays allowed, though nmap marks one version script,
iec61850-mms, intrusive; the code says so.

* src/codebud_sec/scope.py (_NMAP_WIDE_CATEGORIES, _NMAP_WIDE_SAMPLES):
Remove.
(_NMAP_SCRIPT_DBS, _NMAP_REFUSED_CATEGORIES, _NMAP_ENTRY)
(_nmap_scripts, _nmap_select): New.
(_nmap_script_problem): Judge the scripts selected.
(_nmap): Judge -sC and -A as default.
* tests/test_scope.py (SCRIPT_DB, script_db): New fixture.
(test_ordinary_scripts_run): -sC and default are refused now.
(test_scripts_beyond_scanning_are_refused)
(test_default_scripts_hold_external_ones)
(test_without_nmaps_script_database_no_script_is_judged): New tests.
* docs/chapters/development.texi: Describe them.
Phase 10, step 5 plan, PR 5 (T0 and T1 rules).

whois has a parser: one query, passive like a DNS lookup (out of scope
it asks); a server given with -h is judged too; an AS number names no
host.  masscan's --rate is capped at 1000 packets a second (its default
is 100), so a scan does not take the client's network down.  curl and
wget may not send a file's contents to a target -- curl -T, @file data,
form fields and headers, wget --post-file and --body-file -- so files of
the project (evidence, other engagements) never leave for a target;
data written in the step itself is fine.

* src/codebud_sec/scope.py (MASSCAN_MAX_RATE): New.
(_masscan): Refuse a faster or unreadable --rate.
(_curl_sends_a_file): New.
(_curl, _wget): Refuse sending a file.
(_WHOIS_VALUE, _WHOIS_FLAGS, _whois): New.
(PARSERS): Add whois.
* tests/test_scope.py, tests/test_scope_web.py: Test them.
* docs/chapters/architecture.texi: Mention whois.
Phase 10, step 5 plan, PR 5: tiers T0 (lookups) and T1 (scanning) of
the accepted plan (D14i); exploitation (T2) waits for the scope fence.

The profile runs no command and no web search until an engagement is
open (engagement = true); the engagement's scope guard then reads every
host each network step contacts and refuses what is out of scope.  Mode
ask; privacy private, since locked refuses network steps; encrypted;
integrity required.  Reading results is harmless; every network tool is
powerful and asked each time: dig, host, whois, nmap, masscan, nc,
ncat, curl, wget, whatweb, nikto, gobuster, ffuf, feroxbuster, sslscan,
testssl.sh.  The profile refuses listeners (nc -l, ncat --listen,
--broker, --chat), which catch shells.  Its prompt states the rules the
guard enforces, so plans meet them the first time.

* src/codebud_sec/data/profiles/security-pentest.toml: New profile.
* src/codebud_sec/data/profiles/security-pentest/: Its policy files.
* src/Makefile.am (nobase_dist_python_DATA): Install them.
* tests/test_security_pentest.py: New file.
* tests/test_profiles.py (test_builtins): List it.
* docs/chapters/configuration.texi (Profiles): Document it.
* docs/chapters/development.texi: List the test file.
* NEWS: Mention it.
@mkhellat

Copy link
Copy Markdown
Owner Author

Review summary: changes requested

One full sweep over bea0efb: conditions → scenarios → each checked against the code → De Morgan.

Conditions. Under security-pentest:

  • Refused ⇔ no provided("engagement") ∨ the core refuses it (allow-list, refused arguments) ∨ scope (an unreadable target ∨ a target ∉ scope ∨ the window closed ∨ a selected nmap script ∈ {exploit, brute, dos, intrusive, fuzzer, broadcast, external, targets-*} ∨ masscan rate > 1000 ∨ a file sent to a target).
  • Asked ⇔ powerful (every network tool) ∨ a passive target ∉ scope.

The invariants behind them:

  1. Scripts nmap runs ⊆ scripts judged against nmap's real database.
  2. Data sent to a target ⊆ data written in the step.
  3. No DoS.

Correct, verified (1738 tests; each new refusal checked to fire for its stated reason):

  • The engagement hook: bound methods compare equal, so provided() follows activate and deactivate.
  • The script evaluator: precedence not > and > or/,; not is taken over all scripts. Results match nmap 7.99's database: -sC → http-open-proxy, socks-open-proxy.
  • No shell: shlex.split then subprocess.run(argv) (sandbox.py:181,200), so | and < can't carry a file.
  • The listener refusals, the rate cap (including inf and nan), and whois.

Defects (each reproduced as ALLOWED on bea0efb):

  1. nmap --datadir ./mine (invariant 1). nmap then loads its scripts and script.db from a directory a step can write, so an innocuously named script runs any Lua, while the guard judges the system database. Fix: refuse --datadir.
  2. feroxbuster --data @secrets.txt (invariant 2). feroxbuster reads @file bodies the way curl does. Fix: refuse --data @….
  3. nikto's DoS tests (invariant 3). -Tuning 6 is nikto's denial-of-service class. Whether plain nikto includes it by default can't be checked here, since nikto isn't installed. Fix: require -Tuning, either listing classes without 6 or excluding 6 (x6).

Residual, not a defect: a few discovery scripts take --script-args that name a DNS domain to query (dns-srv-enum.domain). Those queries go to the resolver, as passive lookups do, and they are documented as such.

Verdict: changes requested. The fixes come as new commits, then a re-review.

Review of #21.  nmap --datadir loads scripts and script.db from the
directory given, which a step can write: an innocuously named script
there runs any Lua while the guard judges the system's database.

* src/codebud_sec/scope.py (_nmap): Refuse --datadir.
Say why, and what --script-args can still name (a DNS domain for the
resolver).
* tests/test_scope.py (test_nmap_datadir_is_refused): New test.
Review of #21.  feroxbuster --data @file sends the file as the request
body, as curl does; it is refused like curl's.  nikto's tuning class 6
is its denial-of-service tests, outside the scanning tier: nikto now
needs -Tuning leaving it out (x6, or a list of classes without 6).
The profile's prompt and the manual say so.

* src/codebud_sec/scope.py (_feroxbuster): Refuse --data @file.
(_nikto): Require -Tuning without class 6.
* src/codebud_sec/data/profiles/security-pentest.toml (prompt): Ask for
-Tuning x6.
* tests/test_scope_web.py: Give nikto -Tuning x6.
(test_feroxbuster_data_from_a_file_is_refused)
(test_nikto_without_leaving_out_dos_is_refused)
(test_nikto_leaving_out_dos_runs): New tests.
* docs/chapters/configuration.texi (Profiles): Document them and nmap
--datadir.
@mkhellat

Copy link
Copy Markdown
Owner Author

Re-review summary: fixed, ready to merge once CI passes

Second sweep, over f1c22d0.

Finding Fix Verified
1. nmap --datadir loads scripts and script.db from a writable directory 353a7ea: refused; the code also records the --script-args DNS-domain residual was allowed, now refused
2. feroxbuster --data @file sends a file f1c22d0: refused like curl's @file; literal --data a=1 still runs was allowed, now refused
3. nikto's DoS class (tuning 6) f1c22d0: -Tuning is required and must leave out 6 (x6, or a list without 6). The profile's prompt and the manual say so. no -Tuning, 6, 126, x12 and x refused; x6, x16, 123b and 0 run

This sweep found nothing new. The nikto rule's logic, refused ⇔ tuning is None ∨ (6 ∈ tuning) ≠ inverted, was checked across all four combinations of "contains 6" and "inverted".

1749 tests pass, ruff is clean, make info builds without warnings, and make distcheck passes. Verdict: correct. I'll merge with a merge commit once CI passes on all jobs, macOS included.

@mkhellat

Copy link
Copy Markdown
Owner Author

Merging. All three review findings are fixed and re-verified: nmap --datadir, feroxbuster --data @file, and nikto's DoS tuning class. CI is green on all five jobs at f1c22d0, macOS included. main has not moved since the branch was cut. A merge commit keeps the six commits as written.

@mkhellat
mkhellat merged commit 8db5e63 into main Nov 12, 2026
5 checks passed
@mkhellat
mkhellat deleted the feat/security-pentest branch November 12, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant